


//import hybrid.data.Weight;
//import hybrid.data.Weights;
//import hybrid.tictactoe.AIPlayer;

import java.text.*;
import java.util.*;

public class NeuralNetwork {
	//Template from http://kunuk.wordpress.com/2010/10/24/hybrid-ai-example-with-java-tictactoe-reinforcement-learning-and-nn/
		
	public final boolean DEBUG = false;
	//public final boolean isTrained = AIPlayer.isTrained;

	final int INPUT=0;
	final int HIDDEN=1;
	final int OUTPUT=2;
	
	final DecimalFormat df;
	final Random rand;
	final double epsilon = Double.MIN_VALUE;

	// this framework only supports one hidden layer
	final ArrayList<Neuron> inputLayer = new ArrayList<Neuron>();
	final ArrayList<Neuron> hiddenLayer = new ArrayList<Neuron>();
	final ArrayList<Neuron> outputLayer = new ArrayList<Neuron>();
	final Neuron bias = new Neuron();

	// for weight update all
	final HashMap<String, Double> weightUpdate = new HashMap<String, Double>();

	/**
	 *  Tic tac toe
	 *  xs is the input, action is the output
	 *  first nine input is 0 or 1 for fields occupied with hero pieces
	 *  next nine input is 0 or 1 for fields occupied villain pieces
	 *  
	 */
	final int[] layers; // input, hidden, output
	final int randomWeightMultiplier = 1;

	final double learningRate = 0.5f;
	final double momentum = 0.9f;
	
	public NeuralNetwork(int input, int hidden, int output) {
		layers = new int[]{ input, hidden, output };
		rand = new Random();
		
		df = new DecimalFormat("#.0#");		

		/**
		 * create all Neurons and connections connections are created in the
		 * neuron class
		 */
		for (int i = 0; i < layers.length; i++) {
			if (i == INPUT) { // input layer
				for (int j = 0; j < layers[INPUT]; j++) {
					Neuron neuron = new Neuron();
					inputLayer.add(neuron);
				}
			} else if (i == HIDDEN) { // hidden layer
				for (int j = 0; j < layers[HIDDEN]; j++) {
					Neuron neuron = new Neuron();
					neuron.addInConnectionsS(inputLayer);
					neuron.addBiasConnection(bias);
					hiddenLayer.add(neuron);
				}
			}

			else if (i == OUTPUT) { // output layer
				for (int j = 0; j < layers[OUTPUT]; j++) {
					Neuron neuron = new Neuron();
					neuron.addInConnectionsS(hiddenLayer);
					neuron.addBiasConnection(bias);
					outputLayer.add(neuron);
				}
			} else {
				System.out.println("!Error NeuralNetwork init");
			}
		}

		// initialize random weights
		for (Neuron neuron : hiddenLayer) {
			ArrayList<Connection> connections = neuron.getAllInConnections();
			for (Connection conn : connections) {
				conn.setWeight(getRandom());
			}
		}
		for (Neuron neuron : outputLayer) {
			ArrayList<Connection> connections = neuron.getAllInConnections();
			for (Connection conn : connections) {
				conn.setWeight(getRandom());
			}
		}

		// reset id counters
		Neuron.counter = 0;
		Connection.counter = 0;	
	}
	
	// random
	double getRandom() {
		return randomWeightMultiplier * ((rand.nextDouble() * 2) - 1); // [-1;1[
	}

	/**
	 * 
	 * @param inputs
	 *            There is equally many neurons in the input layer as in input
	 *            variables
	 */
	public void setInput(double inputs[]) {

		for (int i = 0; i < inputLayer.size(); i++) {
			inputLayer.get(i).setOutput(inputs[i]);
		}
	}

	public double[] getOutput() {
		double[] outputs = new double[outputLayer.size()];
		for (int i = 0; i < outputLayer.size(); i++)
			outputs[i] = outputLayer.get(i).getOutput();
		return outputs;
	}

	/**
	 * Calculate the output of the neural network based on the input The forward
	 * operation
	 */
	public void activate() {
		for (Neuron n : hiddenLayer) {
			n.calculateOutput();
		}

		for (Neuron n : outputLayer) {
			n.calculateOutput();
		}
	}

	public void printAllWeights() {
		// weights for the hidden layer
		for (Neuron n : hiddenLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double w = con.getWeight();
				System.out.println("n=" + n.id + " c=" + con.id + " w="
						+ df.format(w));
			}
		}
		// weights for the output layer
		for (Neuron n : outputLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double w = con.getWeight();
				System.out.println("n=" + n.id + " c=" + con.id + " w="
						+ df.format(w));
			}
		}
	}
	
	public void setAllWeights(Weights ws) 
	{
		weightUpdate.clear();
		
		for (Weight w : ws.weights) 
			weightUpdate.put(weightKey(w.nodeId,w.connectionId), w.weight);	
		
		updateAllWeights();
	}
	
	public Weights getAllWeights() {
		Weights weights = new Weights();
		
		// weights for the hidden layer
		for (Neuron n : hiddenLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double w = con.getWeight();
				Weight weight = new Weight(n.id, con.id, w);
				weights.weights.add(weight);				
			}
		}
		// weights for the output layer
		for (Neuron n : outputLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double w = con.getWeight();
				Weight weight = new Weight(n.id, con.id, w);
				weights.weights.add(weight);
			}
		}
		
		return weights;
	}

	String weightKey(int neuronId, int conId) {
		return "N" + neuronId + "_C" + conId;
	}

	/**
	 * Take from hash table and put into all weights
	 */
	public void updateAllWeights() {
		// update weights for the output layer
		for (Neuron n : outputLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				String key = weightKey(n.id, con.id);
				double newWeight = weightUpdate.get(key);
				con.setWeight(newWeight);
			}
		}
		// update weights for the hidden layer
		for (Neuron n : hiddenLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				String key = weightKey(n.id, con.id);
				double newWeight = weightUpdate.get(key);
				con.setWeight(newWeight);
			}
		}
	}

	/**
	 * 
	 * @param expectedOutput
	 *            first calculate the partial derivative of the error with
	 *            respect to each of the weight leading into the output neurons
	 *            bias is also updated here
	 */
	public void applyBackpropagation(double expectedOutput[]) {
		// error check
		for (int i = 0; i < expectedOutput.length; i++) {
			double d = expectedOutput[i];
			if(d<0 || d>1)			{
				// only propagate back valid range
				if(d<0)
					expectedOutput[i] = 0+epsilon;
				else
					expectedOutput[i] = 1-epsilon;
			}			
		}
					
		int i = 0;
		for (Neuron n : outputLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double ak = n.getOutput();
				double ai = con.leftNeuron.getOutput();
				double desiredOutput = expectedOutput[i];

				double partialDerivative = -ak * (1 - ak) * ai
						* (desiredOutput - ak);
				double deltaWeight = -learningRate * partialDerivative;
				double newWeight = con.getWeight() + deltaWeight;
				con.setDeltaWeight(deltaWeight);
				con.setWeight(newWeight + momentum * con.getPrevDeltaWeight());
			}
			i++;
		}

		// update weights for the hidden layer
		for (Neuron n : hiddenLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double aj = n.getOutput();
				double ai = con.leftNeuron.getOutput();
				double sumKoutputs = 0;
				int j = 0;
				for (Neuron out_neu : outputLayer) {
					double wjk = out_neu.getConnection(n.getId()).getWeight();
					double desiredOutput = (double) expectedOutput[j];
					double ak = out_neu.getOutput();
					j++;
					sumKoutputs = sumKoutputs
							+ (-(desiredOutput - ak) * ak * (1 - ak) * wjk);
				}

				double partialDerivative = aj * (1 - aj) * ai * sumKoutputs;
				double deltaWeight = -learningRate * partialDerivative;
				double newWeight = con.getWeight() + deltaWeight;
				con.setDeltaWeight(deltaWeight);
				con.setWeight(newWeight + momentum * con.getPrevDeltaWeight());
			}
		}
	}

	
	public void applyBackpropagation(double expectedOutput, int index) {
		// error check
		double d = expectedOutput;
		if (d < 0 || d > 1) {
			// only propagate back valid range
			if (d < 0)
				expectedOutput = 0 + epsilon;
			else
				expectedOutput = 1 - epsilon;
		}
					
		int i = 0;
		for (Neuron n : outputLayer) {
			if(index!=i)
				continue;
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double ak = n.getOutput();
				double ai = con.leftNeuron.getOutput();
				double desiredOutput = expectedOutput;

				double partialDerivative = -ak * (1 - ak) * ai
						* (desiredOutput - ak);
				double deltaWeight = -learningRate * partialDerivative;
				double newWeight = con.getWeight() + deltaWeight;
				con.setDeltaWeight(deltaWeight);
				con.setWeight(newWeight + momentum * con.getPrevDeltaWeight());
			}
			i++;
		}

		// update weights for the hidden layer
		for (Neuron n : hiddenLayer) {
			ArrayList<Connection> connections = n.getAllInConnections();
			for (Connection con : connections) {
				double aj = n.getOutput();
				double ai = con.leftNeuron.getOutput();
				double sumKoutputs = 0;
				int j = 0;
				for (Neuron out_neu : outputLayer) {
					if(index!=j)
						continue;
					double wjk = out_neu.getConnection(n.getId()).getWeight();
					double desiredOutput = (double) expectedOutput;
					double ak = out_neu.getOutput();
					j++;
					sumKoutputs = sumKoutputs
							+ (-(desiredOutput - ak) * ak * (1 - ak) * wjk);
				}

				double partialDerivative = aj * (1 - aj) * ai * sumKoutputs;
				double deltaWeight = -learningRate * partialDerivative;
				double newWeight = con.getWeight() + deltaWeight;
				con.setDeltaWeight(deltaWeight);
				con.setWeight(newWeight + momentum * con.getPrevDeltaWeight());
			}
		}
	}
}
